Skip to content

Python: add agent-framework-hosting-responses channel#5639

Open
eavanvalkenburg wants to merge 2 commits intomicrosoft:feature/python-hostingfrom
eavanvalkenburg:feat/hosting-channel-responses
Open

Python: add agent-framework-hosting-responses channel#5639
eavanvalkenburg wants to merge 2 commits intomicrosoft:feature/python-hostingfrom
eavanvalkenburg:feat/hosting-channel-responses

Conversation

@eavanvalkenburg
Copy link
Copy Markdown
Member

Motivation and Context

Implements §7 ("Built-in channels — Responses") of SPEC-002 (merged via #5549). Adds the OpenAI Responses-compatible channel so an AgentFrameworkHost can be reached by any Responses SDK / OpenAI client.

Description

Adds the new agent-framework-hosting-responses package (python/packages/hosting-responses/) with:

  • ResponsesChannel — mounts POST /responses (configurable), parses the OpenAI Responses request shape into ChannelRequest, and serializes the agent's reply back into a Responses payload (sync or streaming SSE).
  • _parsing.py — request/response translation, isolation-key derivation from previous_response_id / safety_identifier, and option scrubbing seam.
  • Tests covering parsing, channel construction, end-to-end round-trips, run-hook integration.

Stack

PR-3 of 9. Depends on #PR-2 (feat/hosting-core). Until PR-2 merges, this PR's diff includes that commit too — afterwards the diff shrinks to just this package.

Contribution Checklist

  • The code builds clean without any errors or warnings
  • The PR follows the Contribution Guidelines
  • All unit tests pass, and I have added new tests where possible
  • Is this a breaking change? No — new package.

@moonbox3 moonbox3 added documentation Improvements or additions to documentation python labels May 5, 2026
Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated Code Review

Reviewers: 4 | Confidence: 77% | Result: All clear

Reviewed: Correctness, Security Reliability, Test Coverage, Design Approach


Automated review by eavanvalkenburg's agents

@eavanvalkenburg eavanvalkenburg force-pushed the feat/hosting-channel-responses branch from 200aa2a to 6f8cf42 Compare May 5, 2026 09:00
eavanvalkenburg and others added 2 commits May 5, 2026 11:08
New ``agent-framework-hosting`` package implementing ADR 0026 / SPEC-002:
the channel-neutral host that lets a single ``Agent`` (or ``Workflow``)
fan out across multiple wire protocols ("channels") behind one Starlette
ASGI app.

Surface (re-exported from ``agent_framework_hosting``):

- ``AgentFrameworkHost`` — wraps a hostable target, mounts channels onto
  an ASGI app, owns per-isolation-key ``AgentSession`` reuse, threads
  request context (``response_id`` / ``previous_response_id``) into
  context providers via an ``ExitStack`` of ``bind_request_context``
  calls, and exposes an opt-in Hypercorn ``serve()`` helper (extra
  ``[serve]``).
- ``Channel`` protocol + ``ChannelContribution`` — the surface a channel
  package implements (routes, lifespans, identity hooks, …).
- ``ChannelRequest`` / ``ChannelSession`` / ``ChannelIdentity`` /
  ``ChannelPush`` / ``ChannelCommand[Context]`` / ``ChannelRunHook`` /
  ``ChannelStreamTransformHook`` / ``DeliveryReport`` /
  ``HostedRunResult`` / ``ResponseTarget`` / ``ResponseTargetKind`` /
  ``apply_run_hook`` — channel-side dataclasses + helpers.
- ``IsolationKeys`` + ``ISOLATION_HEADER_USER`` / ``..._CHAT`` +
  ``get/set/reset_current_isolation_keys`` — the host's ASGI middleware
  reads the ``x-agent-{user,chat}-isolation-key`` headers off each
  inbound request and exposes them to the agent stack via a
  ``ContextVar`` so storage-side providers (e.g.
  ``FoundryHostedAgentHistoryProvider``) can apply per-tenant
  partitioning without channels having to forward anything.

Includes 45 unit tests covering the host, channel contributions,
isolation contextvar, and shared types. Registers the package in
``python/pyproject.toml`` ``[tool.uv.sources]`` and adds the matching
pyright ``executionEnvironments`` entry for tests.

Hypercorn is an optional dependency (``[serve]`` extra); the soft import
in ``serve()`` is annotated for pyright since it isn't on the default
install.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
New ``agent-framework-hosting-responses`` package implementing the
OpenAI Responses-shaped HTTP channel for the Hosting framework. Mounts
``POST /responses`` (and a ``/responses/{response_id}`` GET) onto an
``AgentFrameworkHost`` and translates the OpenAI Responses wire shape
to/from the channel-neutral ``ChannelRequest`` / ``HostedRunResult``
plumbing.

Surface (re-exported from ``agent_framework_hosting_responses``):

- ``ResponsesChannel`` -- concrete ``Channel`` implementation. Owns the
  Starlette route(s), parses inbound JSON into ``ChannelRequest``, runs
  the optional ``ChannelRunHook``, calls back into the
  ``ChannelContext`` to invoke the agent target, builds Responses
  envelopes (sync JSON or SSE), and respects
  ``DeliveryReport.include_originating`` so cross-channel push routes
  only ack to the originating Responses caller.
- The minted ``response_id`` is propagated via the host's ContextVar
  machinery so storage-side history providers (e.g.
  ``FoundryHostedAgentHistoryProvider``) persist envelopes against the
  same id the channel returns.
- 48 unit tests covering route wiring, parsing of each Responses input
  shape, hook composition, sync vs streaming paths, and originating
  vs non-originating delivery branches.

Registers the package in ``python/pyproject.toml`` ``[tool.uv.sources]``
and adds the matching pyright ``executionEnvironments`` entry.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@eavanvalkenburg eavanvalkenburg force-pushed the feat/hosting-channel-responses branch from 6f8cf42 to ee183ae Compare May 5, 2026 09:10
# user-iso companion header is consumed at the host level by
# ``_FoundryIsolationASGIMiddleware`` so the channel never has
# to import Foundry-specific types.
chat_iso = request.headers.get("x-agent-chat-isolation-key")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be trusting this header straight off the wire? The comment above says it is "platform-injected" by Foundry, but the channel itself does no validation. If ResponsesChannel is mounted anywhere outside Foundry, or in front of the Foundry middleware in the ASGI stack, any HTTP caller can set x-agent-chat-isolation-key: <someone-else> and end up sharing that user's per-conversation session bucket. Right? Should we have, at minimum, a sanity check that a host-level guard is enforced before this point, and probably a docstring note that mounting this channel requires the isolation middleware in front?

# ``previous_response_id`` and the storage chain walks. We pass
# both anchors via ``ChannelRequest.attributes`` so the host
# can pick them up without a channel-specific contract.
previous_response_id: str | None = None
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring for response_id_factory (lines 92-101) says id backends like Foundry storage embed partition keys in the id. We pass the user-supplied previous_response_id straight to the factory so the new record co-locates with the existing partition. Have we thought about what happens when a caller supplies an arbitrary resp_* id pointing at someone else's partition? Without a host-side check that the chain is owned by this caller, the factory ends up writing the new record into a partition the caller does not own. Is partition ownership enforced inside the factory itself, or somewhere upstream we are relying on?

return

completed_text = accumulated
report = await self._ctx.deliver_response(request, HostedRunResult(text=accumulated))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens to deliver_response and any push-channel fan-out when the stream raises mid-flight? The non-streaming path always calls deliver_response, but here on failure we emit response.failed and return without ever invoking it. The client has already seen partial deltas on the wire, but host-side history, idempotency, and any non-originating push targets see nothing for this turn. Next turn the chain anchored on this response_id will be inconsistent with what the user actually saw. Should we be calling deliver_response with accumulated (or with an explicit failed marker) before returning, so host state matches wire state?

"frequency_penalty",
"presence_penalty",
"logit_bias",
"instructions",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instructions is in _RESPONSES_OPTION_PASSTHROUGH but parse_responses_request short-circuits on it at line 218 (if key == "instructions": continue) because it is already prepended as a system message. Doesn't that mean the entry here is dead? Doesn't look like a bug today, but the next person who adds another consumer of _RESPONSES_OPTION_PASSTHROUGH will quietly ship instructions twice (system message and option)? Can we drop it from the set?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants